package com.company.nuwa.redis.wrapper;

import cn.hutool.core.util.NumberUtil;
import com.company.nuwa.redis.common.JsonRedisSerializer;
import com.company.nuwa.redis.constants.RedisExtScript;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.lang.Nullable;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * <p>redis value operations wrapper</p >
 *
 * @author Nikola Tesla
 * @version 1.0
 * @date 2020/12/1 10:26
 */
@Slf4j
public class RedisValueOperationsWrapper {

    private final ValueOperations<String, String> valueOperations;
    private final StringRedisTemplate stringRedisTemplate;

    public RedisValueOperationsWrapper(StringRedisTemplate stringRedisTemplate) {
        this.valueOperations = stringRedisTemplate.opsForValue();
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * Set the {@code value} and expiration {@code timeout} for {@code key}.
     *
     * @param key     must not be {@literal null}.
     * @param value   must not be {@literal null}.
     * @param timeout the key expiration timeout.
     * @param unit    must not be {@literal null}.
     * @see <a href="https://redis.io/commands/setex">Redis Documentation: SETEX</a>
     */
    public void setString(String key, String value, long timeout, TimeUnit unit) {
        valueOperations.set(key, value, timeout, unit);
    }

    /**
     * Get the value of {@code key}.
     *
     * @param key must not be {@literal null}.
     * @return {@literal null} when used in pipeline / transaction.
     * @see <a href="https://redis.io/commands/get">Redis Documentation: GET</a>
     */
    public String getString(String key) {
        return valueOperations.get(key);
    }

    /**
     * Get the value of {@code key}.
     *
     * @param key must not be {@literal null}.
     * @return {@literal null} when used in pipeline / transaction.
     * @see <a href="https://redis.io/commands/get">Redis Documentation: GET</a>
     */
    public Long getLong(String key) {
        String val = valueOperations.get(key);
        return NumberUtil.parseLong(val);
    }

    /**
     * Set the {@code value} and expiration {@code timeout} for {@code key}.
     *
     * @param key     must not be {@literal null}.
     * @param value   must not be {@literal null}.
     * @param timeout the key expiration timeout.
     * @param unit    must not be {@literal null}.
     * @see <a href="https://redis.io/commands/setex">Redis Documentation: SETEX</a>
     */
    public <T> void setBean(String key, T value, long timeout, TimeUnit unit) {
        valueOperations.set(key, ValueConverter.getString(value), timeout, unit);
    }

    /**
     * Get the value of {@code key}.
     *
     * @param key       must not be {@literal null}.
     * @param classType class type
     * @return {@literal null} when used in pipeline / transaction.
     * @see <a href="https://redis.io/commands/get">Redis Documentation: GET</a>
     */
    public <T> T getBean(String key, Class<T> classType) {
        String value = valueOperations.get(key);
        return ValueConverter.getBean(value, classType);
    }

    /**
     * Get multiple {@code keys}. Values are returned in the order of the requested keys.
     *
     * @param keys      must not be {@literal null}.
     * @param classType class type
     * @return {@literal null} when used in pipeline / transaction.
     * @see <a href="https://redis.io/commands/mget">Redis Documentation: MGET</a>
     */
    public <T> List<T> multiGet(Collection<String> keys, Class<T> classType) {
        List<String> values = valueOperations.multiGet(keys);
        if (Objects.isNull(values)) {
            return Lists.newArrayList();
        }
        return values.stream()
                .filter(Objects::nonNull)
                .map(value -> ValueConverter.getBean(value, classType))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    /**
     * Get multiple {@code keys}. Values are returned in the order of the requested keys.
     *
     * @param keys            must not be {@literal null}.
     * @param classType       class type
     * @param redisSerializer redis serializer
     * @return {@literal null} when used in pipeline / transaction.
     * @see <a href="https://redis.io/commands/mget">Redis Documentation: MGET</a>
     */
    public <T> List<T> multiGet(Collection<String> keys, Class<T> classType, GenericJackson2JsonRedisSerializer redisSerializer) {
        List<String> values = valueOperations.multiGet(keys);
        if (Objects.isNull(values)) {
            return Lists.newArrayList();
        }
        return values.stream()
                .filter(Objects::nonNull)
                .map(value -> JsonRedisSerializer.deserialize(redisSerializer, value.getBytes(), classType))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    /**
     * Set the {@code value} and expiration {@code timeout} for {@code key}.
     *
     * @param key     must not be {@literal null}.
     * @param value   must not be {@literal null}.
     * @param timeout the key expiration timeout.
     * @param unit    must not be {@literal null}.
     * @see <a href="https://redis.io/commands/setex">Redis Documentation: SETEX</a>
     */
    public <T> void setBeanList(String key, List<T> value, long timeout, TimeUnit unit) {
        valueOperations.set(key, ValueConverter.getString(value), timeout, unit);
    }

    /**
     * Get the value of {@code key}.
     *
     * @param key       must not be {@literal null}.
     * @param classType class type
     * @return {@literal null} when used in pipeline / transaction.
     * @see <a href="https://redis.io/commands/get">Redis Documentation: GET</a>
     */
    public <T> List<T> getBeanList(String key, Class<T> classType) {
        String s = valueOperations.get(key);
        return ValueConverter.getBeanList(s, classType);
    }

    /**
     * 对String类型的key进行递增递减并设置过期值的原子脚本, 初始值为0， 每次递增+1
     * <p>
     * 若该key已存在，则时间不再变更，即只有初始化时，才设置时间
     *
     * @param key           key
     * @param expireSeconds 过期秒值
     * @return 缓存key对应的最新值
     */
    @Nullable
    public Long incrementKeyExpire(String key, long expireSeconds) {
        String value = stringRedisTemplate.execute(RedisExtScript.STRING_KEY_INCREMENT_EXPIRE.getRedisScript(), Collections.singletonList(key), "1",
                String.valueOf(expireSeconds)
        );
        if (Objects.isNull(value)) {
            return null;
        }
        return Long.parseLong(value);
    }

    /**
     * 对String类型的key进行递增递减并设置过期指定指定时间的原子脚本
     * 时间小于当前时间，会导致ttl为-1, 这里不进行校验
     * <p>
     * 若该key已存在，则时间不再变更，即只有初始化时，才设置时间
     *
     * @param key      key
     * @param expireAt 指定过期的具体时间
     * @return 缓存key对应的最新值
     */
    @Nullable
    public Long incrementKeyExpireAt(String key, Date expireAt) {
        String value = stringRedisTemplate.execute(RedisExtScript.STRING_KEY_INCREMENT_EXPIRE_AT.getRedisScript(), Collections.singletonList(key), "1",
                // 这个单位是秒
                String.valueOf(expireAt.getTime() / 1000)
        );
        if (Objects.isNull(value)) {
            return null;
        }
        return Long.parseLong(value);
    }

    /**
     * Increment an integer value stored as string value under {@code key} by {@code delta}.
     *
     * @param key   must not be {@literal null}.
     * @param delta
     * @return {@literal null} when used in pipeline / transaction.
     * @see <a href="https://redis.io/commands/incrby">Redis Documentation: INCRBY</a>
     */
    @Nullable
    public Long increment(String key, long delta, long expiredSeconds) {
        Long val = valueOperations.increment(key, delta);
        stringRedisTemplate.expire(key, expiredSeconds, TimeUnit.SECONDS);
        return val;
    }

    /**
     * Decrement an integer value stored as string value under {@code key} by {@code delta}.
     *
     * @param key   must not be {@literal null}.
     * @param delta
     * @return {@literal null} when used in pipeline / transaction.
     * @see <a href="https://redis.io/commands/incrby">Redis Documentation: DECRBY</a>
     */
    @Nullable
    public Long decrement(String key, long delta, long expiredSeconds) {
        Long val = valueOperations.decrement(key, delta);
        stringRedisTemplate.expire(key, expiredSeconds, TimeUnit.SECONDS);
        return val;
    }
}
